package com.itany.shop.util;

import com.itany.shop.exception.DataAccessException;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 石小俊
 * @date 2023年06月05日 13:56
 * JDBC模板方法:对JDBC访问数据库的操作进行封装,简化开发
 */
public class JDBCTemplate<T> {

    /**
     * 执行一个DML操作。即:insert、delete、update
     * 对于增删改操作,使用ps.executeUpdate()执行sql
     * 对于整个方法而言,主要在于sql与参数不一致,其他代码基本相同
     * 可以将不一致的sql与参数设置为当前模板方法的参数
     * 由于sql的参数数量不定,因此,可以使用可变长参数进行封装
     * @param sql    执行的SQL语句
     * @param params SQL所需要的参数列表
     */
    public void update(String sql, Object... params) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = JDBCUtil.getConnection();
            ps = conn.prepareStatement(sql);
            // 设置参数
            // 可变长参数用于接收实际传递的参数的
            // 因此,可变长参数有几个,即表示需要设置几个参数
            // 此时可以遍历可变长参数
            for (int i = 0; i < params.length; i++) {
                // 取出当前遍历的参数
                Object param = params[i];
                // 设置参数时,设置什么类型的参数?
                // 如果直接使用setObject,对于下属类型而言,效率会降低
                // 可以对当前参数类型进行判断,通过向下转型进行判断
                if (param instanceof String) {
                    ps.setString(i + 1, (String) param);
                } else if (param instanceof Integer) {
                    ps.setInt(i + 1, (Integer) param);
                } else if (param instanceof Double) {
                    ps.setDouble(i + 1, (Double) param);
                } else {
                    // 由于数据类型太多了,一个个的判断代码会过多
                    // 我们可以对常见类型进行判断
                    // 其他类型可以直接使用Object表示
                    ps.setObject(i + 1, param);
                }
            }
            // 执行DML操作
            ps.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            // 在业务层的事务操作中需要使用Connection
            // 因此,此时释放资源时不要释放Connection
            JDBCUtil.close(null, ps, null);
        }
    }

    /**
     * 执行一个查询操作
     * 查询操作会返回一个结果,其结果可能是一条,也可能是多条
     * 因此,可以通过集合来接收返回的结果
     * 对于查询结果而言,我们需要将返回的结果映射成Java对象
     * 具体映射成哪一个Java对象,目前不知道,只有在实际调用时才能确定
     * 因此,通过接口作为模板方法的参数,调用者在实际调用的时候再做具体的实现
     * @param sql    执行的SQL语句
     * @param rm     行映射,将查询结果映射成具体的Java对象
     * @param params 执行SQL所需要的参数
     * @return 根据行映射结果生成具体的Java对象集合
     */
    public List<T> query(String sql, RowMapper rm, Object... params) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<T> list = new ArrayList<>();
        try {
            conn = JDBCUtil.getConnection();
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                // 取出当前遍历的参数
                Object param = params[i];
                // 设置参数时,设置什么类型的参数?
                // 如果直接使用setObject,对于下属类型而言,效率会降低
                // 可以对当前参数类型进行判断,通过向下转型进行判断
                if (param instanceof String) {
                    ps.setString(i + 1, (String) param);
                } else if (param instanceof Integer) {
                    ps.setInt(i + 1, (Integer) param);
                } else if (param instanceof Double) {
                    ps.setDouble(i + 1, (Double) param);
                } else {
                    // 由于数据类型太多了,一个个的判断代码会过多
                    // 我们可以对常见类型进行判断
                    // 其他类型可以直接使用Object表示
                    ps.setObject(i + 1, param);
                }
            }
            rs = ps.executeQuery();
            // 执行查询操作
            while (rs.next()) {
                T t = (T) rm.mapRow(rs);
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            JDBCUtil.close(null, ps, rs);
        }
    }

    /**
     * 执行DML操作,用于保存数据的同时返回生成的主键值
     * 此时生成PreparedStatement的方式与之前的不一样,可以定义一个接口由调用者做具体实现
     * 返回的主键值可能只有一个,也可以存在多个,定义一个KeyHolder类用于接收所有保存的主键值
     * @param psc
     * @param keyHolder
     */
    public void update(PreparedStatementCreator psc, KeyHolder keyHolder) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = JDBCUtil.getConnection();
            // 调用者在调用方法时做具体的实现
            // 在实现的时候,会提供构建状态集时所需要的sql与参数
            ps = psc.createPreparedStatement(conn);
            ps.executeUpdate();

            // 保存后数据库中自动生成对应的主键
            // 获取生成的主键的值
            rs = ps.getGeneratedKeys();
            // 准备一个集合,用于存储所有自动生成的主键的值
            List<Object> list = new ArrayList<>();

            // 通过结果集元数据取获取生成的主键值
            ResultSetMetaData rsmd = rs.getMetaData();
            // 获取生成的列的总数
            int columnCount = rsmd.getColumnCount();
            if (rs.next()) {
                // 在按行读取数据的时候,遍历所有列的值
                // 将每一列的值存放到对应集合中
                // 每一列对应一个主键值
                for (int i = 1; i <= columnCount; i++) {
                    list.add(rs.getObject(i));
                }
            }
            keyHolder.setKeyList(list);
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException("数据访问异常");
        } finally {
            JDBCUtil.close(null, ps, rs);
        }
    }
}
